﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Security.Cryptography;
using System.IO;
using System.Drawing;
using System.Drawing.Imaging;
using System.Drawing.Drawing2D;
using System.Drawing.Text;

namespace Mindfor.Web
{
	/// <summary>
	/// Provides function for captcha encryption and decryption.
	/// </summary>
	public static class CaptchaManager
	{
		static AesManaged s_aes;
		static ICryptoTransform s_aesEncryptor;
		static ICryptoTransform s_aesDecryptor;
		
		/// <summary>
		/// Gets or sets key that will be used to save captcha key in ViewData. Default = CaptchaKey.
		/// </summary>
		public static string ViewDataKey { get; set; }

		/// <summary>
		/// Gets or sets name for key html hidden field. Default = CaptchaKey.
		/// </summary>
		public static string HtmlKeyName { get; set; }

		/// <summary>
		/// Gets or sets name for text html textbox. Default = CaptchaText.
		/// </summary>
		public static string HtmlTextName { get; set; }

		/// <summary>
		/// Initializes static variables.
		/// </summary>
		static CaptchaManager()
		{
			ViewDataKey = "CaptchaKey";
			HtmlKeyName = "CaptchaKey";
			HtmlTextName = "CaptchaText";

			s_aes = new AesManaged();
			s_aesEncryptor = s_aes.CreateEncryptor();
			s_aesDecryptor = s_aes.CreateDecryptor();
		}

		/// <summary>
		/// Generates random captcha text.
		/// </summary>
		/// <param name="length">Length of captcha text in range (0, 10].</param>
		public static string GenerateText(int length = 5)
		{
			// check
			if (length <= 0 || length > 10)
				throw new ArgumentException("Captcha length must be in range (0, 10]", "length");

			// generate
			string s = "";
			Random r = new Random();

			for (int i=0; i<length; i++)
			{
				bool number = r.Next(2) == 1;
				if (number)
					s += r.Next(10);
				else
					s += (char)((int)'A' + r.Next(24));
			}

			return s;
		}

		/// <summary>
		/// Encrypts captcha text to public key that can be used in url.
		/// </summary>
		/// <param name="text">Text to encrypt.</param>
		/// <returns>Public key.</returns>
		public static string EncryptTextToKey(string text)
		{
			// check arguments
			if (String.IsNullOrEmpty(text))
				throw new ArgumentNullException("text");
			
			// declare vars
			MemoryStream outputStream = null;
			CryptoStream cryptoStream = null;
			StreamWriter writer = null;
			
			try
			{
				// create the streams used for encryption.
				outputStream = new MemoryStream();
				cryptoStream = new CryptoStream(outputStream, s_aesEncryptor, CryptoStreamMode.Write);
				writer = new StreamWriter(cryptoStream);

				// write all data to the stream.
				writer.Write(text);
			}
			finally
			{
				// clean up
				if (writer != null)
					writer.Close();
				if (cryptoStream != null)
					cryptoStream.Close();
				if (outputStream != null)
					outputStream.Close();

				s_aes.Clear();
			}

			// convert encrypted bytes to key string
			byte[] encrypted = outputStream.ToArray();
			string res = Convert.ToBase64String(encrypted);
			return res;
		}

		/// <summary>
		/// Decrypts public key to captcha text.
		/// </summary>
		/// <param name="key">Key to decrypt.</param>
		/// <returns>Decrypted captcha text.</returns>
		public static string DecryptTextFromKey(string key)
		{
			// check arguments
			if (String.IsNullOrEmpty(key))
				throw new ArgumentNullException("key");

			// declare vars
			MemoryStream inputStream = null;
			CryptoStream cryptoStream = null;
			StreamReader reader = null;

			// convert key string to bytes
			byte[] encrypted = Convert.FromBase64String(key);
			string text = null;

			try
			{
				// create the streams used for encryption.
				inputStream = new MemoryStream(encrypted);
				cryptoStream = new CryptoStream(inputStream, s_aesDecryptor, CryptoStreamMode.Read);
				reader = new StreamReader(cryptoStream);

				// read the decrypted bytes from the decrypting stream and place them in a string.
				text = reader.ReadToEnd();
			}
			finally
			{
				// clean up
				if (inputStream != null)
					inputStream.Close();
				if (cryptoStream != null)
					cryptoStream.Close();
				if (reader != null)
					reader.Close();

				s_aes.Clear();
			}

			return text;
		}

		/// <summary>
		/// Generates captcha image.
		/// </summary>
		/// <param name="minWidth">Image minimim width.</param>
		/// <param name="minHeight">Image minimum height.</param>
		/// <param name="fontSize">Text font size in pixels.</param>
		/// <param name="text">Captcha text.</param>
		/// <returns>Captcha image.</returns>
		public static Bitmap GenerateImage(int minWidth, int minHeight, int fontSize, string text)
		{
			// check base arguments
			if (text == null)
				throw new ArgumentNullException("text");
			if (fontSize < 4)
				throw new ArgumentException("Font size must equals or more that 4.", "fontSize");

			// compute real symbol size
			SizeF symbolSize;
			using (Graphics g = Graphics.FromHwnd(IntPtr.Zero))
			{
				using (Font font = new Font("Courier New", fontSize))
				{
					symbolSize = g.MeasureString("W", font);
				}
			}
			symbolSize = new SizeF(symbolSize.Width * 1.2F, symbolSize.Height * 1.5F);

			// compute image size
			int width = (int)Math.Ceiling(symbolSize.Width * text.Length);
			int height = (int)Math.Ceiling(symbolSize.Height);
			if (width < minWidth)
				width = minWidth;
			if (height < minHeight)
				height = minHeight;

			// create bitmap
			Bitmap img = new Bitmap(width, height, PixelFormat.Format32bppArgb);
			using (Graphics g = Graphics.FromImage(img))
			{
				// text fonts
				Font[] fonts = new Font[] {
					new Font("Arial", fontSize - 1, FontStyle.Bold),
					new Font("Courier New", fontSize, FontStyle.Italic | FontStyle.Bold),
					new Font("Times New Roman", fontSize + 1, FontStyle.Bold | FontStyle.Underline),
					new Font("Arial", fontSize + 1, FontStyle.Italic | FontStyle.Underline)};

				// text brushes
				Brush[] brushes = new Brush[] {
					Brushes.DarkGray,
					Brushes.Gray,
					Brushes.LightSkyBlue,
					Brushes.LightSalmon	};

				// draw background
				Rectangle imgRect = new Rectangle(0, 0, img.Width, img.Height);
				using (LinearGradientBrush bgBrush = new LinearGradientBrush(imgRect, Color.White, Color.LightGray, 0F))
				{
					ColorBlend cb = new ColorBlend();
					cb.Colors = new Color[]
					{
						Color.White,
						Color.LightGray,
						Color.White
					};
					cb.Positions = new float[] { 0.0F, 0.5F, 1.0F };
					bgBrush.InterpolationColors = cb;
					g.FillRectangle(bgBrush, imgRect);
				}

				// draw text
				g.InterpolationMode = InterpolationMode.HighQualityBilinear;
				g.SmoothingMode = SmoothingMode.AntiAlias;
				g.PixelOffsetMode = PixelOffsetMode.HighQuality;
				g.TextRenderingHint = TextRenderingHint.ClearTypeGridFit;

				Random r = new Random();
				StringFormat format = new StringFormat(StringFormatFlags.NoWrap | StringFormatFlags.NoClip);
				float placeWidth = (float)width / text.Length;
				float offsetX = 0;
				
				for (int i=0; i<text.Length; i++)
				{
					string c = text[i].ToString();
					Font font = fonts[r.Next(fonts.Length)];
					Brush brush = brushes[r.Next(brushes.Length)];

					SizeF charSize = g.MeasureString(c, font);
					float left = (placeWidth - charSize.Width) / 2;
					float top = (height - charSize.Height) / 2;
					float x = offsetX + left * (float)r.NextDouble() * 2F;
					float y = top * (float)r.NextDouble() * 2F;
					PointF charCenter = new PointF(x + charSize.Width / 2, y + charSize.Height / 2);
					
					Matrix matrix = new Matrix();
					matrix.RotateAt(r.Next(-40, 40), charCenter);
					matrix.Shear((float)(r.NextDouble() / 10 - 0.08), 0);
					g.MultiplyTransform(matrix, MatrixOrder.Prepend);
					
					g.DrawString(c, font, brush, x, y);
					offsetX += placeWidth;
					g.ResetTransform();
				}

				// dispose resources
				for (int i=0; i<fonts.Length; ++i)
					fonts[i].Dispose();
				g.Flush();
			}

			// return image
			return img;
		}
	}
}
